package biolabs.model.motion

import biolabs.core.{BioObject, Interaction, WrappedDelegate}
import biolabs.model.GridBioContainer
import biolabs.space.ContinuousPosition
import biolabs.space.DiscretePosition
import biolabs.space.GridShape
import biolabs.space.GridShapedObject

import java.util.LinkedList

import scala.collection.JavaConversions._

class Motion[
	T <: BioObject with GridShapedObject[P, C] with Movable[T, P],
	P <: DiscretePosition[P, C],
	C <: ContinuousPosition[C, P]
]
extends Interaction[GridBioContainer[T, P, C], Unit, Unit, MotionDelegate[P], T] {

	type Parent = GridBioContainer[T, P, C]

	/**
	 * Performs the motion of contained bio-objects.
	 */
	override def perform(
			parent: Parent,
			participants: Iterable[WrappedDelegate[MotionDelegate[P], T]],
			notifyProgress: Unit=>Unit
	): Unit = {
		for(md <- participants) {
			val moved = moveBy(parent, md.owner, md.move)
			md.move = moved
			md.commit()
		}
	}

	private def moveBy(parent: Parent, child: T, tomove: P): P = {
		val zero = tomove.zero
		var tm = tomove
		var moved = zero
		while(!tm.isEqualTo(zero)) {
			val step = extractStep(tm)
			if(doDirectMove(parent, child, step)) {
				tm = tm.minus(step);
				moved = moved.plus(step);
			} else
				return moved;
		}
		return moved
	}

	/**
	 * Returns a position whose each coordinate is from {-1,0,1} and
	 * is closest (with minimal angle) to {@code move}.
	 *
	 * If {@code move} is a zero vector, the behavior is undefined.
	 */
	private def extractStep(move: P): P = {
		assert(!move.isEqualTo(move.zero()))

		val dim = move.getDimension();

		// get the direction (1/-1/0) in each coordinate
		val dirs = for(i <- 0 until dim) yield move.getCoordinate(i).compareTo(0)

		// choose a step whose direction is
		// the closest to the direction of move
		var bestStep: P = move.zero()
		var maxcos = 0.0
		val l = move.length()
		val stepCoords = new Array[Int](dim)
		val n = 1<<dim
		for(indicators <- 0 until n) {
			for(i <- 0 until dim)
				stepCoords(i) = if((indicators & 1<<i) != 0) dirs(i) else 0

			val step = move.newInstance(stepCoords)
			val ip = move.innerProduct(step).intValue()
			if(ip != 0) {
				val cos = ip/(l*step.length())
				if(cos > maxcos){
					bestStep = step;
					maxcos = cos;
				}
			}
		}

		assert(!bestStep.isEqualTo(move.zero()))
		return bestStep;
	}

	/**
	 * Moves {@code child} by {@code move} from its original position
	 * {@code refp}, if the space to be newly occupied by {@code child}
	 * is not occupied by something else (or out of this environment's
	 * range). {@code child} will be either moved by exactly {@code move},
	 * or not moved at all.
	 *
	 * @param child the child bio-object to be moved
	 * @param move the vector of the movement
	 * @param refp the original position of {@code child}.
	 * @param shape <tt>child</tt>'s shape
	 *
	 * @return {@code true} if {@code child} was moved,
	 * {@code false} otherwise.
	 */
	private def doDirectMove(parent: Parent, child: T, move: P): Boolean = {
		val shape = child.getShape()

		// check the feasibility of the move
		for(p <- shape.getElements()) {
			val pos = p.plus(move)
			if(!parent.isValidPos(pos))
				return false;
			val owner = parent.getChildAt(pos);
			if(owner != null && owner != child)
				return false;
		}

		// create local copy of a portion of the mask
		val span = shape.getSpan();
		val childMask = parent.getChildMask()
		val window = childMask.copyOf(span.a, span.b);

		// perform the move
		val capt = new LinkedList[P]();
		for(p <- shape.getElements()) {
			val npos=p.plus(move)
			val ppos=p.minus(move).minus(span.a)

			val l = childMask.setField(npos, child);
			if(l == null)
				capt.add(npos);
			else
				assert(l eq child);
			if(!window.inRange(ppos) || window.at(ppos)!=child)
				childMask.setField(p, null.asInstanceOf[T]);
		}
		shape.translate(move)

		// notify other traits about the captured space
		parent.notifyCapture(capt);

		// the newly released elements will stay empty until
		// diffusion or redistribution from their neighbors occurs

		return true;
	}
}
